/*
 * Hibiscus is free software; you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published
 * by the Free Software Foundation; either version 3 of the License,
 * or (at your option) any later version.
 *
 * Hibiscus is distributed in the hope that it will be useful, but
 * WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
 * General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Hibiscus; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307
 * USA
 *
 * Copyright (C) 2003 Tetete - http://tetete.hu.
 */
package hu.tetete.hibiscus.dal;

import hu.tetete.hibiscus.dal.PropertyDao.PropertyDef.PropertyType;
import hu.tetete.hibiscus.expression.Expression;
import hu.tetete.hibiscus.expression.TrueExpression;
import hu.tetete.hibiscus.model.MultiSelectProperty;
import hu.tetete.hibiscus.model.OneSelectProperty;
import hu.tetete.hibiscus.model.Property;
import hu.tetete.hibiscus.model.PropertyGroup;
import hu.tetete.hibiscus.propertyvalue.SelectPropertyValue;
import hu.tetete.hibiscus.util.EnumUtil;
import hu.tetete.hibiscus.util.ExpressionUtil;

import java.util.ArrayList;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

import android.content.ContentValues;
import android.database.Cursor;
import android.database.sqlite.SQLiteDatabase;
import android.database.sqlite.SQLiteOpenHelper;
import android.util.Log;

/**
 * TODO javadoc.
 */
public class PropertyDao {

    /** Logger's tag. */
    private static final String TAG = PropertyDao.class.getName();

    /** TODO javadoc. */
    public static class PropertyDef {
        /** The name of the table which stores properties. */
        public static final String TABLE_NAME = "property";

        /** The columns of the database table. */
        public enum TABLE_COLUMNS {
            /** The id column, a unique identifier of each property. */
            ID,
            /** The name columns which stores the name of property. */
            NAME,
            /** The type of the property. */
            PROPERTY_TYPE,
            /** The expression which controls the visibility. */
            EXPRESSION,
            /** The id of the parent property. */
            PARENT_ID
        }

        /** TODO javadoc. */
        public enum PropertyType {
            /** TODO javadoc. */
            GROUP,
            /** TODO javadoc. */
            ONE_SELECT,
            /** TODO javadoc. */
            MULTI_SELECT,

        }

        /** TODO javadoc. */
        static final String TABLE_CREATE =
            "CREATE TABLE " + TABLE_NAME + " (" +
                " " + TABLE_COLUMNS.ID             + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                " " + TABLE_COLUMNS.NAME           + " TEXT," +
                " " + TABLE_COLUMNS.PROPERTY_TYPE  + " TEXT," +
                " " + TABLE_COLUMNS.EXPRESSION     + " TEXT," +
                " " + TABLE_COLUMNS.PARENT_ID      + " INTEGER" +
            ");";

    }

    /** TODO javadoc. */
    public static class SelectPropertyOptionDef {
        /** The name of the table which stores properties. */
        public static final String TABLE_NAME = "select_property_option";

        /** The columns of the database table. */
        public enum TABLE_COLUMNS {
            /** The id column, a unique identifier of each property option. */
            ID,
            /** The select property option. */
            NAME,
            /** The id of property to which the option is relating. */
            PROPERTY_ID
        }

        /** TODO javadoc. */
        static final String TABLE_CREATE =
            "CREATE TABLE " + TABLE_NAME + " (" +
                " " + TABLE_COLUMNS.ID             + " INTEGER PRIMARY KEY AUTOINCREMENT," +
                " " + TABLE_COLUMNS.NAME           + " TEXT," +
                " " + TABLE_COLUMNS.PROPERTY_ID    + " INTEGER" +
            ");";

    }

    /**
     * Generates the needed tabels and datasets for properties.
     * @param db The database
     */
    public static void generateDb(final SQLiteDatabase db) {
        Log.i(TAG, "Create database: taxon");
        db.execSQL(PropertyDef.TABLE_CREATE);
        db.execSQL(SelectPropertyOptionDef.TABLE_CREATE);
        final long firstGroupId = db.insert(PropertyDef.TABLE_NAME, null,
                createPropertyContent("First Group", PropertyDef.PropertyType.GROUP, new TrueExpression(), null));
        final long firstOneSelect = db.insert(PropertyDef.TABLE_NAME, null,
                createPropertyContent("First One Select", PropertyDef.PropertyType.ONE_SELECT, new TrueExpression(), null));
        final long firstOneSelectFirstOption = db.insert(SelectPropertyOptionDef.TABLE_NAME, null,
                createSelectPropertyOption("First option", Long.valueOf(firstOneSelect)));
        final long firstOneSelectSecondOption = db.insert(SelectPropertyOptionDef.TABLE_NAME, null,
                createSelectPropertyOption("Second option", Long.valueOf(firstOneSelect)));
    }

    /**
     * Upgrades the database. TODO javadoc.
     * @param db The database
     */
    public static void upgradeDb(final SQLiteDatabase db) {
        db.execSQL("DROP TABLE IF EXISTS " + PropertyDef.TABLE_NAME);
        db.execSQL("DROP TABLE IF EXISTS " + SelectPropertyOptionDef.TABLE_NAME);
        generateDb(db);
    }

    public static ContentValues createPropertyContent(final String name, final PropertyType propertyType, final Expression expression, final Long parentId) {
        final ContentValues insertValues = new ContentValues();
        insertValues.put(PropertyDef.TABLE_COLUMNS.NAME.name(),          name);
        insertValues.put(PropertyDef.TABLE_COLUMNS.PROPERTY_TYPE.name(), propertyType.name());
        insertValues.put(PropertyDef.TABLE_COLUMNS.EXPRESSION.name(),    ExpressionUtil.serializeExpression(expression));
        insertValues.put(PropertyDef.TABLE_COLUMNS.PARENT_ID.name(),     parentId);
        return insertValues;
    }

    public static ContentValues createSelectPropertyOption(final String name, final Long propertyId) {
        final ContentValues insertValues = new ContentValues();
        insertValues.put(SelectPropertyOptionDef.TABLE_COLUMNS.NAME.name(),        name);
        insertValues.put(SelectPropertyOptionDef.TABLE_COLUMNS.PROPERTY_ID.name(), propertyId);
        return insertValues;
    }

    private static class PropertyWrapper {

        private final Long              id;
        private final Property<?>       property;
        private       List<Property<?>> children;


        /**
         * Creates a new propertyWrapper.
         * @param id The id
         * @param property The property
         */
        PropertyWrapper(final Long id, final Property<?> property) {
            super();
            this.id       = id;
            this.property = property;
            this.children = null;
        }

        /**
         * @return the id
         */
        public Long getId() {
            return this.id;
        }

        /**
         * @return the property
         */
        public Property<?> getProperty() {
            return this.property;
        }

        /**
         * @return the children
         */
        public List<Property<?>> getChildren() {
            return this.children;
        }

        /**
         * @param children the children to set
         */
        public void setChildren(final List<Property<?>> children) {
            this.children = children;
        }

    }

    private final TaxonDatabaseOpenHelper    openHelper;
    private final Map<Long, PropertyWrapper> propertyCache    = new HashMap<Long, PropertyWrapper>();
    private       List<Property<?>>          topPropertyCache;


    /**
     * @param openHelper The {@link SQLiteOpenHelper database open helper}
     */
    public PropertyDao(final TaxonDatabaseOpenHelper openHelper) {
        super();
        this.openHelper = openHelper;
    }

//    /**
//     * TODO javadoc.
//     */
//    public PropertyDao() {
//        addTop(new PropertyGroup(newId(), "First Group"),
//                add(new OneSelectProperty(newId(), "First Select",
//                        new TrueExpression(), Arrays
//                                .asList(new SelectPropertyValue[] {
//                                        new SelectPropertyValue(newId(),
//                                                "First option"),
//                                        new SelectPropertyValue(newId(),
//                                                "Second option") }))));
//        addTop(new PropertyGroup(newId(), "Second Group"),
//                add(new OneSelectProperty(newId(), "Second First Select",
//                        new TrueExpression(), Arrays
//                                .asList(new SelectPropertyValue[] {
//                                        new SelectPropertyValue(newId(),
//                                                "First option"),
//                                        new SelectPropertyValue(newId(),
//                                                "Second option") }))));
//        addTop(new OneSelectProperty(newId(), "Very very long name for this element TopSelect",
//                new TrueExpression(), Arrays
//                        .asList(new SelectPropertyValue[] {
//                                new SelectPropertyValue(newId(),
//                                        "First option"),
//                                new SelectPropertyValue(newId(),
//                                        "Second option") })));
//        addTop(new MultiSelectProperty(newId(), "Test Multi",
//                new TrueExpression(), Arrays
//                        .asList(new SelectPropertyValue[] {
//                                new SelectPropertyValue(newId(),
//                                        "First option"),
//                                new SelectPropertyValue(newId(),
//                                        "Second option") })));
//
//    }
//
//    private long idGenerator = 0;
//
//    private Long newId() {
//        return Long.valueOf(this.idGenerator++);
//    }
//
//    private PropertyWrapper addTop(final Property property,
//            final PropertyWrapper... children) {
//        return add(property, true, children);
//    }
//
//    private PropertyWrapper add(final Property property,
//            final PropertyWrapper... children) {
//        return add(property, false, children);
//    }
//
//    private PropertyWrapper add(final Property property,
//            final boolean topLevel, final PropertyWrapper... children) {
//        final List<Property> childrenProperties;
//        if (children != null) {
//            childrenProperties = new ArrayList<Property>(children.length);
//            for (final PropertyWrapper child : children) {
//                childrenProperties.add(child.getProperty());
//            }
//        } else {
//            childrenProperties = Collections.<Property> emptyList();
//        }
//
//        if (topLevel) {
//            this.topProperties.add(property);
//        }
//
//        final PropertyWrapper propertyWrapper = new PropertyWrapper(
//                property.getId(), property, childrenProperties);
//        this.properties.put(property.getId(), propertyWrapper);
//        return propertyWrapper;
//    }

    /**
     * @param id TODO javadoc.
     * @return TODO javadoc.
     */
    public Property<?> getPropertyById(final Long id) {
        final PropertyWrapper propertyWrapper = getPropertyWrapperById(id);
        return propertyWrapper == null ? null : propertyWrapper.getProperty();
    }

    private PropertyWrapper getPropertyWrapperById(final Long id) {
        final PropertyWrapper propertyWrapper = this.propertyCache.get(id);
        if (propertyWrapper == null) {
            final Property<?> property = getPropertyByIdFromDb(id);
            if (property != null) {
                final PropertyWrapper newWrapper = new PropertyWrapper(property.getId(), property);
                this.propertyCache.put(property.getId(), newWrapper);
                return newWrapper;
            }
            return null;
        }
        return propertyWrapper;
    }

    private Property<?> getPropertyByIdFromDb(final Long id) {
        Log.d(TAG, "getPropertyByIdFromDb: " + id);
        final SQLiteDatabase db      = this.openHelper.getReadableDatabase();
        final String[]       columns = EnumUtil.getEnumNames(PropertyDef.TABLE_COLUMNS.class);
        final Cursor         cursor  = db.query(PropertyDef.TABLE_NAME, columns, PropertyDef.TABLE_COLUMNS.ID + "=" + id, null, null, null, null);
        try {
            if (cursor == null) {
                return null;
            }

            cursor.moveToFirst();
            return populateObject(cursor);
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    /**
     * @param parentId TODO javadoc.
     * @return TODO javadoc.
     */
    public List<Property<?>> getPropertyChildrenById(final Long parentId) {
        if (parentId == null) {
            if (this.topPropertyCache == null) {
                loadTopPropertyCache();
            }
            return this.topPropertyCache;
        }
        final PropertyWrapper propertyWrapper = getPropertyWrapperById(parentId);
        if (propertyWrapper == null) {
            throw new IllegalArgumentException("The parent id is not value: " + parentId);
        }
        if (propertyWrapper.getChildren() == null) {
            loadPropertyWrapperChildren(propertyWrapper);
        }
        return propertyWrapper.getChildren();
    }

    private void loadTopPropertyCache() {
        Log.d(TAG, "loadTopPropertyCache");
        this.topPropertyCache = loadPropertyChildren(null);
    }

    private void loadPropertyWrapperChildren(final PropertyWrapper propertyWrapper) {
        Log.d(TAG, "loadPropertyWrapperChildren");
        propertyWrapper.setChildren(loadPropertyChildren(propertyWrapper.getId()));
    }

    private List<Property<?>> loadPropertyChildren(final Long id) {
        final SQLiteDatabase db      = this.openHelper.getReadableDatabase();
        final String[]       columns = EnumUtil.getEnumNames(PropertyDef.TABLE_COLUMNS.class);
        final Cursor         cursor  = db.query(PropertyDef.TABLE_NAME, columns, PropertyDef.TABLE_COLUMNS.PARENT_ID + (id == null ? " is null" : " = " + id), null, null, null, PropertyDef.TABLE_COLUMNS.NAME.name());
        try {
            if (cursor == null) {
                return Collections.<Property<?>>emptyList();
            }
            cursor.moveToFirst();
            final List<Property<?>> children = new ArrayList<Property<?>>();
            while (!cursor.isAfterLast()) {
                final Long childId = Long.valueOf(cursor.getLong(PropertyDef.TABLE_COLUMNS.ID.ordinal()));
                final PropertyWrapper cachedProperty = this.propertyCache.get(childId);
                if (cachedProperty != null) {
                    children.add(cachedProperty.getProperty());
                } else {
                    final Property<?> property = populateObject(cursor);
                    if (property != null) {
                        this.propertyCache.put(property.getId(), new PropertyWrapper(property.getId(), property));
                        children.add(property);
                    }
                }
                cursor.moveToNext();
            }
            return children;
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }
    }

    private Property<?> populateObject(final Cursor cursor) {
        final Long         id               = Long.valueOf(cursor.getLong(PropertyDef.TABLE_COLUMNS.ID.ordinal()));
        final String       name             = cursor.getString(PropertyDef.TABLE_COLUMNS.NAME.ordinal());
        final PropertyType propertyType     = PropertyType.valueOf(cursor.getString(PropertyDef.TABLE_COLUMNS.PROPERTY_TYPE.ordinal()));
        final String       expressionString = cursor.getString(PropertyDef.TABLE_COLUMNS.EXPRESSION.ordinal());

        final Expression expression = ExpressionUtil.deserializeExpression(expressionString);

        Log.i(TAG, "Populate Property Id: " + id + " name: " + name);

        switch (propertyType) {
            case GROUP : {
                return new PropertyGroup(id, name, expression);
            }
            case MULTI_SELECT : {
                return new MultiSelectProperty(id, name, expression, getOptionsForSelectProperty(id));
            }
            case ONE_SELECT : {
                return new OneSelectProperty(id, name, expression, getOptionsForSelectProperty(id));
            }
        }
        throw new IllegalArgumentException();
    }

    private List<SelectPropertyValue> getOptionsForSelectProperty(final Long id) {
        final Cursor cursor  = this.openHelper.getReadableDatabase().query(SelectPropertyOptionDef.TABLE_NAME,
                EnumUtil.getEnumNames(SelectPropertyOptionDef.TABLE_COLUMNS.class),
                SelectPropertyOptionDef.TABLE_COLUMNS.PROPERTY_ID + " = " + id, null, null, null, SelectPropertyOptionDef.TABLE_COLUMNS.NAME.name());
        try {
            if (cursor == null) {
                return Collections.<SelectPropertyValue>emptyList();
            }
            cursor.moveToFirst();

            final List<SelectPropertyValue> options = new ArrayList<SelectPropertyValue>();

            while (!cursor.isAfterLast()) {
                final Long   optionId   = Long.valueOf(cursor.getLong(SelectPropertyOptionDef.TABLE_COLUMNS.ID.ordinal()));
                final String optionName = cursor.getString(SelectPropertyOptionDef.TABLE_COLUMNS.NAME.ordinal());

                options.add(new SelectPropertyValue(optionId, optionName));

                cursor.moveToNext();
            }
            return options;
        } finally {
            if (cursor != null) {
                cursor.close();
            }
        }



    }

}
